package com.gitee.wsl.security.hash.bcrypt

import com.gitee.wsl.ext.base.Arrays.arraycopy

/**
 * Encoder for the custom Base64 variant of BCrypt (called Radix64 here). It has the same rules as Base64 but uses a
 * different mapping table than the various RFCs
 *
 *
 * According to Wikipedia:
 *
 * <blockquote>
 * Unix stores password hashes computed with crypt in the /etc/passwd file using radix-64 encoding called B64. It uses a
 * mostly-alphanumeric set of characters, plus . and /. Its 64-character set is "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".
 * Padding is not used.
</blockquote> *
 */
interface Radix64Encoder {
    /**
     * Encode given raw byte array to a Radix64 style, UTF-8 encoded byte array.
     *
     * @param rawBytes to encode
     * @return UTF-8 encoded string representing radix64 encoded data
     */
    fun encode(rawBytes: ByteArray): ByteArray

    /**
     * From a UTF-8 encoded string representing radix64 encoded data as byte array, decodes the raw bytes from it.
     *
     * @param utf8EncodedRadix64String from a string get it with `"m0CrhHm10qJ3lXRY.5zDGO".getBytes(StandardCharsets.UTF8)`
     * @return the raw bytes encoded by this utf-8 radix4 string
     */
    fun decode(utf8EncodedRadix64String: ByteArray): ByteArray

    /*
     *  Licensed to the Apache Software Foundation (ASF) under one or more
     *  contributor license agreements.  See the NOTICE file distributed with
     *  this work for additional information regarding copyright ownership.
     *  The ASF licenses this file to You under the Apache License, Version 2.0
     *  (the "License"); you may not use this file except in compliance with
     *  the License.  You may obtain a copy of the License at
     *
     *     http://www.apache.org/licenses/LICENSE-2.0
     *
     *  Unless required by applicable law or agreed to in writing, software
     *  distributed under the License is distributed on an "AS IS" BASIS,
     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     *  See the License for the specific language governing permissions and
     *  limitations under the License.
     */
    /**
     * A mod of Square's Okio Base64 encoder
     *
     *
     * Original author: Alexander Y. Kleymenov
     *
     * @see [Okio](https://github.com/square/okio/blob/okio-parent-1.15.0/okio/src/main/java/okio/Base64.java)
     */
    class Default : Radix64Encoder {
        
        override fun encode(`in`: ByteArray): ByteArray {
            return encode(
                `in`,
                MAP
            )
        }

        override fun decode(`in`: ByteArray): ByteArray {
            // Ignore trailing '=' padding and whitespace from the input.
            var limit = `in`.size
            while (limit > 0) {
                val c = `in`[limit - 1]
                if (c != '='.code.toByte() && c != '\n'.code.toByte() && c != '\r'.code.toByte() && c != ' '.code.toByte() && c != '\t'.code.toByte()) {
                    break
                }
                limit--
            }

            // If the input includes whitespace, this output array will be longer than necessary.
            val out = ByteArray((limit * 6L / 8L).toInt())
            var outCount = 0
            var inCount = 0

            var word = 0
            for (pos in 0..<limit) {
                val c = `in`[pos]

                val bits: Int
                if (c == '.'.code.toByte() || c == '/'.code.toByte() || (c >= 'A'.code.toByte() && c <= 'z'.code.toByte()) || (c >= '0'.code.toByte() && c <= '9'.code.toByte())) {
                    bits = DECODE_TABLE[c.toInt()].toInt()
                } else if (c == '\n'.code.toByte() || c == '\r'.code.toByte() || c == ' '.code.toByte() || c == '\t'.code.toByte()) {
                    continue
                } else {
                    throw IllegalArgumentException("invalid character to decode: " + c)
                }

                // Append this char's 6 bits to the word.
                word = (word shl 6) or (bits.toByte().toInt() and 0xff)

                // For every 4 chars of input, we accumulate 24 bits of output. Emit 3 bytes.
                inCount++
                if (inCount % 4 == 0) {
                    out[outCount++] = (word shr 16).toByte()
                    out[outCount++] = (word shr 8).toByte()
                    out[outCount++] = word.toByte()
                }
            }

            val lastWordChars = inCount % 4
            if (lastWordChars == 1) {
                // We read 1 char followed by "===". But 6 bits is a truncated byte! Fail.
                return ByteArray(0)
            } else if (lastWordChars == 2) {
                // We read 2 chars followed by "==". Emit 1 byte with 8 of those 12 bits.
                word = word shl 12
                out[outCount++] = (word shr 16).toByte()
            } else if (lastWordChars == 3) {
                // We read 3 chars, followed by "=". Emit 2 bytes for 16 of those 18 bits.
                word = word shl 6
                out[outCount++] = (word shr 16).toByte()
                out[outCount++] = (word shr 8).toByte()
            }

            // If we sized our out array perfectly, we're done.
            if (outCount == out.size) return out

            // Copy the decoded bytes to a new, right-sized array.
            val prefix = ByteArray(outCount)
            arraycopy(out, 0, prefix, 0, outCount)
            return prefix
        }

        companion object {
            private val DECODE_TABLE = byteArrayOf(
                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 54, 55, 56, 57,
                58, 59, 60, 61, 62, 63, -1, -1, -1, -2, -1, -1, -1, 2, 3, 4, 5, 6, 7,
                8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
                26, 27, -1, -1, -1, -1, -1, -1, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37,
                38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53
            )

            private val MAP = byteArrayOf(
                '.'.code.toByte(),
                '/'.code.toByte(),
                'A'.code.toByte(),
                'B'.code.toByte(),
                'C'.code.toByte(),
                'D'.code.toByte(),
                'E'.code.toByte(),
                'F'.code.toByte(),
                'G'.code.toByte(),
                'H'.code.toByte(),
                'I'.code.toByte(),
                'J'.code.toByte(),
                'K'.code.toByte(),
                'L'.code.toByte(),
                'M'.code.toByte(),
                'N'.code.toByte(),
                'O'.code.toByte(),
                'P'.code.toByte(),
                'Q'.code.toByte(),
                'R'.code.toByte(),
                'S'.code.toByte(),
                'T'.code.toByte(),
                'U'.code.toByte(),
                'V'.code.toByte(),
                'W'.code.toByte(),
                'X'.code.toByte(),
                'Y'.code.toByte(),
                'Z'.code.toByte(),
                'a'.code.toByte(),
                'b'.code.toByte(),
                'c'.code.toByte(),
                'd'.code.toByte(),
                'e'.code.toByte(),
                'f'.code.toByte(),
                'g'.code.toByte(),
                'h'.code.toByte(),
                'i'.code.toByte(),
                'j'.code.toByte(),
                'k'.code.toByte(),
                'l'.code.toByte(),
                'm'.code.toByte(),
                'n'.code.toByte(),
                'o'.code.toByte(),
                'p'.code.toByte(),
                'q'.code.toByte(),
                'r'.code.toByte(),
                's'.code.toByte(),
                't'.code.toByte(),
                'u'.code.toByte(),
                'v'.code.toByte(),
                'w'.code.toByte(),
                'x'.code.toByte(),
                'y'.code.toByte(),
                'z'.code.toByte(),
                '0'.code.toByte(),
                '1'.code.toByte(),
                '2'.code.toByte(),
                '3'.code.toByte(),
                '4'.code.toByte(),
                '5'.code.toByte(),
                '6'.code.toByte(),
                '7'.code.toByte(),
                '8'.code.toByte(),
                '9'.code.toByte()
            )

            private fun encode(`in`: ByteArray, map: ByteArray): ByteArray {
                val length = 4 * (`in`.size / 3) + (if (`in`.size % 3 == 0) 0 else `in`.size % 3 + 1)
                val out = ByteArray(length)
                var index = 0
                val end = `in`.size - `in`.size % 3
                var i = 0
                while (i < end) {
                    out[index++] = map[(`in`[i].toInt() and 0xff) shr 2]
                    out[index++] = map[((`in`[i].toInt() and 0x03) shl 4) or ((`in`[i + 1].toInt() and 0xff) shr 4)]
                    out[index++] = map[((`in`[i + 1].toInt() and 0x0f) shl 2) or ((`in`[i + 2].toInt() and 0xff) shr 6)]
                    out[index++] = map[(`in`[i + 2].toInt() and 0x3f)]
                    i += 3
                }
                when (`in`.size % 3) {
                    1 -> {
                        out[index++] = map[(`in`[end].toInt() and 0xff) shr 2]
                        out[index] = map[(`in`[end].toInt() and 0x03) shl 4]
                    }

                    2 -> {
                        out[index++] = map[(`in`[end].toInt() and 0xff) shr 2]
                        out[index++] =
                            map[((`in`[end].toInt() and 0x03) shl 4) or ((`in`[end + 1].toInt() and 0xff) shr 4)]
                        out[index] = map[((`in`[end + 1].toInt() and 0x0f) shl 2)]
                    }
                }
                return out
            }
        }
    }
}
